#include "Camera.h"
#include "GL/glut.h"
#include <time.h>
#define _USE_MATH_DEFINES
#include <math.h>

extern int SCREEN_WIDTH;
extern int SCREEN_HEIGHT;

#define ROTATION_DECAY 0.05
#define ROTATION_FRICTION 10
#define SLIDE_DURATION (0.5 * CLOCKS_PER_SEC) // Camera perspective shifts take half a second
#define	ZOOM_FRICTION 0.3 // Camera mouse-wheel zoom velocity reduction is 0.01 per update loop (assuming 30 fps)

Camera::Camera(void)
	: Entity()
{
	target_fov = 90;
	near_clipping_distance = 0.1;
	far_clipping_distance = 100000;

	inMotion = false;
	rotation_velocity = VectorXYZ(0,0,0);
	zoom_speed = 0;
}

Camera::~Camera(void) {
}

// Basic camera functions

void Camera::LookAt(Entity* _focus) {
	VectorXYZ displacement = _focus->position - position;
	focus = _focus;
	distance = displacement.Magnitude();
	RotateTo(displacement);
}

void Camera::SlideTo(Entity* _focus) {
	if (!inMotion) {
		last_focus = focus;
		focus = _focus;
		inMotion = true;
		camera_clock.Reset();
	}
}

void Camera::Zoom(float delta) {
	if (distance / exp(delta) < focus->MinimumCameraDistance())	{
		position += orientation * (distance - focus->MinimumCameraDistance());
		distance = focus->MinimumCameraDistance();
	}
	else {
		position = position + (orientation / exp(delta));
		distance = distance / exp(delta);
	}
}

void Camera::VelocityZoom(float zoom_acceleration) {
	zoom_speed += zoom_acceleration;
}

void Camera::RotateAround(float angle, VectorXYZ axis) {
	RotateBy(angle, axis);
	position = focus->position - distance * orientation;
}

void Camera::RotateAround(float angle, float XCoord, float YCoord, float ZCoord) {
	RotateBy(angle, XCoord, YCoord, ZCoord);
	position = focus->position - distance * orientation;
}

// Mouse Camera Functions

void Camera::SlideTo(MouseState* mouse, std::vector<DrawnEntity*> draw_list) {
	float selection_radius = 100;

	float selection_index = -1; // -1 implies that no legitimate object has been found
	float pixel_distance;
	float least_pixel_distance;
	float distance;
	float least_distance;
	for (int i = 0; i < (int)draw_list.size(); i++) {
		if (LocationToPixel(draw_list[i]->position).z <= 0) continue;
		pixel_distance = (LocationToPixel(draw_list[i]->position) - mouse->position).Magnitude();
		distance = (draw_list[i]->position - position).Magnitude();
		if ((pixel_distance <= selection_radius) && ((selection_index = -1) || (distance <= least_distance))) {
			selection_index = i;
			least_pixel_distance = pixel_distance;
			least_distance = distance;
		}
	}
	if (selection_index > -1)
		SlideTo(draw_list[selection_index]);
}


void Camera::RotateAround(float angle_multiplier, MouseState* mouse) {
	rotation_velocity += mouse->Difference() * angle_multiplier;
}

// Convenience functions

void Camera::CameraOrbitVertical(float angle) {
	RotateAround(angle, up % orientation);
}

void Camera::CameraOrbitHorizontal(float angle) {
	RotateAround(angle, up);
}

VectorXYZ Camera::LocationToPixel(VectorXYZ location) {
	location -= position;

	if (orientation * location <= 0) return NULL;

	float up_projection = (location * up) / distance;
	float right_projection = (location * (orientation % up)) / distance;

	float projection_scaling_factor = (SCREEN_HEIGHT >= SCREEN_WIDTH) ?
		(SCREEN_HEIGHT / 2) / tan(target_fov * M_PI / 360) :
		(SCREEN_WIDTH / 2) / tan(target_fov * M_PI / 360);

	up_projection *= projection_scaling_factor;
	right_projection *= projection_scaling_factor;

	up_projection += (SCREEN_HEIGHT / 2);
	right_projection += (SCREEN_WIDTH / 2);

	return VectorXYZ(right_projection, up_projection, (orientation * location <= 0) ? -1 : 1);
}

// Update function to be run every regular number of game ticks

void Camera::Update() {
	if (!inMotion) { // Is the camera in motion? If it is, we're gonna need to figure out where it's at
		position = focus->position - (orientation * distance); // Track the focus if it's moving

		CameraRotationAnimation();
		CameraZoomAnimation();

		camera_clock.Reset();
	}
	else CameraSlideAnimation();
}

void Camera::CameraRotationAnimation() {
	if (rotation_velocity.Magnitude() != 0) {
		float time_fraction = camera_clock.Lapse() / (float) CLOCKS_PER_SEC;

		rotation_velocity *= exp(log(ROTATION_DECAY) * time_fraction); // Exponential rotation speed decay

		RotateAround(rotation_velocity.Magnitude() * time_fraction,
			(rotation_velocity.x * (up % orientation) -
			rotation_velocity.y * up) % orientation);

		// Applying rotation friction:
		if (rotation_velocity.Magnitude() <= ROTATION_FRICTION)
			rotation_velocity.set(0,0,0);
		else
			rotation_velocity *= (1 - ROTATION_FRICTION / rotation_velocity.Magnitude());
	}
}

void Camera::CameraZoomAnimation() {
	if (zoom_speed != 0) {
		float time_fraction = camera_clock.Lapse() / (float) CLOCKS_PER_SEC;
		if (distance / exp(zoom_speed * time_fraction) < focus->MinimumCameraDistance()) { // Are we too close to the object to zoom in any further?
			position += orientation * (distance - focus->MinimumCameraDistance());
			distance = focus->MinimumCameraDistance();
			zoom_speed = 0;
		}
		else {
			distance = distance / exp(zoom_speed * time_fraction);
			position = focus->position - (orientation * distance);
			// Applying zoom friction:
			if (abs(zoom_speed) <= ZOOM_FRICTION)
				zoom_speed = 0;
			else if (zoom_speed > 0)
				zoom_speed -= ZOOM_FRICTION;
			else
				zoom_speed += ZOOM_FRICTION;
		}
	}
}

void Camera::CameraSlideAnimation() {
	if (camera_clock.Lapse() > SLIDE_DURATION) {
		inMotion = false;
		if (distance < focus->MinimumCameraDistance())
			distance = focus->MinimumCameraDistance();
	}
	else {
		VectorXYZ zoom_out; // If we're moving the camera from a close up shot of a small entity to a much larger entity, we need to zoom out

		if (distance > focus->MinimumCameraDistance())
			zoom_out = VectorXYZ(0,0,0);
		else
			zoom_out = orientation * (focus->MinimumCameraDistance() - distance);

		position =
			last_focus->position - (orientation * distance) +
			((focus->position - last_focus->position) - zoom_out) *
			CameraSmoothing(camera_clock.Lapse() / SLIDE_DURATION);
	}
}

// Camera smoothing function: [0, 1] -> [0,1]: Acts as a mapping between the moment of a camera move and where it's supposed to be
float Camera::CameraSmoothing(float fraction) {
	return pow(fraction, (float)0.33);
}

void Camera::PlaceCamera() {
	gluLookAt(	position.x, position.y, position.z, 
				position.x + orientation.x, position.y + orientation.y, position.z + orientation.z, 
				up.x, up.y, up.z);
}